发布于 

FRIDA辅助分析 OLLVM 字符串加密

OLLVM 字符串加密的详细实现原理

字符串加密是 OLLVM 混淆中最基础也是最实用的 pass 之一。它的目标是将 SO 文件中的明文字符串转换为密文,使得静态分析工具(如 IDA 的 Strings 窗口、strings 命令)无法直接读取敏感信息。

加密流程

OLLVM 字符串加密在 LLVM 编译后端工作,具体流程如下:

源码中的字符串: "https://api.example.com/token"
         ↓ LLVM IR 阶段
识别所有 GlobalVariable 类型的字符串常量
         ↓
对字符串进行加密(XOR / 旋转 / 替换)
         ↓
生成加密后的全局数组: [0x7F, 0x72, 0x69, 0x63, ...]
         ↓
生成解密函数: __decrypt_str_N()
         ↓
在所有引用原始字符串的位置插入解密函数调用
         ↓
最终二进制: 密文数据 + 解密函数 + 调用点

常见的加密方式

  1. 固定密钥 XOR:最简单的方式,所有字符串使用同一个 XOR 密钥
  2. 逐字节递增 XOR:密钥逐字节递增(如 key = 0x5A, 0x5B, 0x5C, …)
  3. 滚动 XOR:每个字节的密钥依赖前一个字节的加密结果
  4. 混合变换:XOR + 位移 + 加法的组合

加密函数在 SO 中的特征

_Z 前缀加密函数

OLLVM 生成的解密函数遵循 C++ 的 name mangling 规则,以 _Z 开头。在 IDA 中可以通过以下方式快速定位:

// 枚举 SO 中所有以 _Z 开头的函数
function findEncryptedFunctions(moduleName) {
    var mod = Process.findModuleByName(moduleName);
    if (!mod) {
        console.log("[-] 模块未加载: " + moduleName);
        return;
    }

    console.log("[*] 模块: " + mod.name +
                " 基地址: " + mod.base);
    console.log("[*] 开始扫描加密函数...\n");

    // 枚举所有导出符号
    var symbols = mod.enumerateSymbols();
    var candidates = [];

    symbols.forEach(function (sym) {
        // 过滤以 _Z 开头且可能是解密函数的符号
        if (sym.name.indexOf("_Z") === 0) {
            // 加密函数通常名称较长(包含编码后的信息)
            if (sym.name.length > 30) {
                candidates.push({
                    name: sym.name,
                    address: sym.address,
                    type: sym.type
                });
            }
        }
    });

    console.log("[+] 找到 " + candidates.length +
                " 个可能的解密函数:");
    candidates.forEach(function (c, i) {
        console.log("  " + (i + 1) + ". " + c.name);
        console.log("     地址: " + c.address);
    });

    return candidates;
}

var candidates = findEncryptedFunctions("libnative.so");

解密函数的代码特征

在 IDA 反汇编视图中,解密函数通常有以下代码模式:

// 典型的 XOR 解密函数伪代码
char* __decrypt_str_42() {
    static char decrypted[32];
    static int initialized = 0;

    if (!initialized) {
        // XOR 密文字节
        char key = 0x5A;
        for (int i = 0; i < 32; i++) {
            decrypted[i] = encrypted_data[i] ^ key;
            key += 3;  // 递增密钥
        }
        initialized = 1;
    }

    return decrypted;
}

Hook 所有字符串操作函数

除了直接 Hook 解密函数外,还可以 Hook C 标准库中的字符串操作函数来捕获运行时的字符串使用:

Hook strcmp / strncmp

// Hook strcmp 捕获所有字符串比较
function hookStringCompare() {
    var strcmpAddr = Module.findExportByName("libc.so", "strcmp");
    var strncmpAddr = Module.findExportByName("libc.so", "strncmp");

    Interceptor.attach(strcmpAddr, {
        onEnter: function (args) {
            var s1 = args[0].readCString();
            var s2 = args[1].readCString();
            if (s1 && s2 && s1.length > 0 && s2.length > 0 &&
                s1.length < 256 && s2.length < 256) {
                console.log("[strcmp] \"" + s1 + "\" vs \"" + s2 + "\"");

                // 打印调用栈
                var bt = Thread.backtrace(this.context,
                    Backtracer.FUZZY).slice(0, 3);
                bt.forEach(function (addr) {
                    console.log("  ← " + DebugSymbol.fromAddress(addr));
                });
            }
        }
    });

    Interceptor.attach(strncmpAddr, {
        onEnter: function (args) {
            var s1 = args[0].readCString();
            var s2 = args[1].readCString();
            var n = args[2].toInt32();
            if (s1 && s2 && n < 256) {
                console.log("[strncmp] \"" +
                    (s1 || "").substring(0, n) + "\" vs \"" +
                    (s2 || "").substring(0, n) + "\" (n=" + n + ")");
            }
        }
    });

    console.log("[+] strcmp/strncmp Hook 已设置");
}

hookStringCompare();

Hook strlen / memcpy / memset

function hookStringOps() {
    // Hook strlen - 可以发现被解密后的字符串长度
    var strlenAddr = Module.findExportByName("libc.so", "strlen");
    Interceptor.attach(strlenAddr, {
        onEnter: function (args) {
            try {
                var s = args[0].readCString();
                if (s && s.length > 3 && s.length < 512) {
                    // 过滤只看有意义的字符串
                    var isPrintable = /^[\x20-\x7E]+$/.test(s);
                    if (isPrintable) {
                        console.log("[strlen] \"" + s +
                            "\" (len=" + s.length + ")");
                    }
                }
            } catch (e) {}
        }
    });

    // Hook memcmp - 常用于签名校验和密钥比较
    var memcmpAddr = Module.findExportByName("libc.so", "memcmp");
    Interceptor.attach(memcmpAddr, {
        onEnter: function (args) {
            var n = args[2].toInt32();
            if (n > 0 && n <= 32) {
                console.log("[memcmp] 比较长度: " + n + " 字节");
                console.log("  ptr1: " + hexdump(args[0], { length: n }));
                console.log("  ptr2: " + hexdump(args[1], { length: n }));
            }
        },
        onLeave: function (retval) {
            if (retval.toInt32() === 0) {
                console.log("  [memcmp] 匹配成功!");
            }
        }
    });
}

hookStringOps();

批量 Dump 解密后的字符串

自动化 Dump 脚本

// 批量 dump 解密字符串的完整脚本
var StringDumper = {
    strings: [],
    dumpCount: 0,

    // 方案 1: Hook OLLVM 解密函数
    hookDecryptFunc: function (moduleName, decryptOffset) {
        var mod = Process.findModuleByName(moduleName);
        var addr = mod.base.add(decryptOffset);

        Interceptor.attach(addr, {
            onLeave: function (retval) {
                this.collectString(retval);
            }
        }.bind(this));

        console.log("[+] 已 Hook 解密函数: " + addr);
    },

    // 方案 2: Hook JNI NewStringUTF(捕获 Java 层可见的字符串)
    hookJNINewString: function () {
        var libart = Process.findModuleByName("libart.so");
        if (!libart) {
            console.log("[-] libart.so 未加载");
            return;
        }

        // 枚举 libart 的导出符号找到 NewStringUTF
        var symbols = libart.enumerateExports();
        symbols.forEach(function (sym) {
            if (sym.name.indexOf("NewStringUTF") !== -1) {
                console.log("[+] 找到: " + sym.name + " @ " + sym.address);
                try {
                    Interceptor.attach(sym.address, {
                        onEnter: function (args) {
                            try {
                                var str = args[1].readCString();
                                if (str && str.length > 0 &&
                                    str.length < 1024) {
                                    this.collectString(args[1]);
                                }
                            } catch (e) {}
                        }
                    }.bind(this));
                } catch (e) {}
            }
        }.bind(this));
    },

    // 收集字符串
    collectString: function (ptr) {
        try {
            var str = ptr.readCString();
            if (str && str.length > 2 && str.length < 1024) {
                this.strings.push({
                    str: str,
                    addr: ptr.toString(),
                    timestamp: Date.now()
                });
                this.dumpCount++;
            }
        } catch (e) {}
    },

    // 输出结果
    print: function () {
        console.log("\n======== 字符串 Dump 结果 ========");
        console.log("共收集 " + this.strings.length + " 个字符串\n");

        // 按字符串内容去重排序
        var unique = {};
        this.strings.forEach(function (item) {
            if (!unique[item.str]) {
                unique[item.str] = item;
            }
        });

        var sorted = Object.values(unique).sort(function (a, b) {
            return a.str.localeCompare(b.str);
        });

        // 分类输出
        var urls = sorted.filter(function (s) {
            return /^https?:\/\//i.test(s.str);
        });
        var keys = sorted.filter(function (s) {
            return /key|secret|token|password|sign/i.test(s.str);
        });
        var paths = sorted.filter(function (s) {
            return /^\//.test(s.str);
        });
        var others = sorted.filter(function (s) {
            return !urls.concat(keys, paths).some(function (u) {
                return u.str === s.str;
            });
        });

        if (urls.length > 0) {
            console.log("[URL]");
            urls.forEach(function (u) {
                console.log("  " + u.str);
            });
        }
        if (keys.length > 0) {
            console.log("[密钥/敏感]");
            keys.forEach(function (k) {
                console.log("  " + k.str);
            });
        }
        if (paths.length > 0) {
            console.log("[路径]");
            paths.forEach(function (p) {
                console.log("  " + p.str);
            });
        }
        if (others.length > 0) {
            console.log("[其他]");
            others.forEach(function (o) {
                console.log("  " + o.str);
            });
        }

        console.log("\n[统计] URL: " + urls.length +
                    " 密钥: " + keys.length +
                    " 路径: " + paths.length +
                    " 其他: " + others.length);
    }
};

// 使用
StringDumper.hookDecryptFunc("libnative.so", 0x2A10);
StringDumper.hookJNINewString();

// 触发 APP 操作后执行
// StringDumper.print();

构建字符串解密辅助脚本

通用解密函数 Hook 模板

// 通用 OLLVM 字符串解密辅助脚本
// 使用方式: 根据实际 APP 修改 moduleName 和 offsets

var CONFIG = {
    moduleName: "libnative.so",
    // 在 IDA 中找到的解密函数偏移列表
    decryptFunctions: [0x2A10, 0x2B40, 0x2C80],
    // 需要过滤的关键词(只显示包含这些词的字符串)
    filterKeywords: ["http", "key", "token", "sign", "secret",
                     "password", "api", "encrypt", "decrypt", "/"],
    // 最大字符串长度限制
    maxLength: 512
};

function createDecryptionHooker(config) {
    var mod = Process.findModuleByName(config.moduleName);
    if (!mod) {
        console.log("[-] 模块 " + config.moduleName + " 未加载");
        return null;
    }

    var collected = [];

    config.decryptFunctions.forEach(function (offset) {
        var addr = mod.base.add(offset);
        try {
            Interceptor.attach(addr, {
                onEnter: function (args) {
                    // 可选: 记录输入参数(密文)
                    this._encrypted = args[0];
                },
                onLeave: function (retval) {
                    try {
                        var str = retval.readCString();
                        if (!str || str.length === 0 ||
                            str.length > config.maxLength) return;

                        // 检查是否可打印
                        var printable = true;
                        for (var i = 0; i < Math.min(str.length, 50); i++) {
                            if (str.charCodeAt(i) < 0x20 &&
                                str.charCodeAt(i) !== 0x09 &&
                                str.charCodeAt(i) !== 0x0A) {
                                printable = false;
                                break;
                            }
                        }
                        if (!printable) return;

                        // 关键词过滤
                        var shouldLog = config.filterKeywords.length === 0;
                        if (!shouldLog) {
                            for (var k = 0; k < config.filterKeywords.length; k++) {
                                if (str.toLowerCase().indexOf(
                                    config.filterKeywords[k]) !== -1) {
                                    shouldLog = true;
                                    break;
                                }
                            }
                        }

                        if (shouldLog) {
                            console.log("[解密] 0x" +
                                offset.toString(16) + " → \"" + str + "\"");

                            var bt = Thread.backtrace(this.context,
                                Backtracer.FUZZY).slice(0, 2);
                            var callerInfo = bt.map(function (a) {
                                return DebugSymbol.fromAddress(a).toString();
                            }).join(" ← ");

                            console.log("  调用: " + callerInfo);
                            collected.push(str);
                        }
                    } catch (e) {}
                }
            });
            console.log("[+] Hook 解密函数 0x" + offset.toString(16));
        } catch (e) {
            console.log("[-] Hook 失败 0x" + offset.toString(16) +
                        ": " + e);
        }
    });

    return {
        getAll: function () { return collected; },
        printUnique: function () {
            var unique = [...new Set(collected)];
            console.log("\n=== 唯一字符串 (" + unique.length + ") ===");
            unique.forEach(function (s) { console.log(s); });
        }
    };
}

var hooker = createDecryptionHooker(CONFIG);

实际案例分析

案例:分析某加固 APP 的 SO 字符串加密

假设我们面对一个使用 OLLVM 字符串加密的 APP,分析步骤如下:

第一步:在 IDA 中定位解密函数

  1. 打开 libnative.so,查看 Strings 窗口,发现几乎没有可读字符串
  2. 查看导出函数列表,找到以 _Z 开头的长名称函数
  3. 交叉引用分析,确认哪些函数被大量调用且返回值被当作字符串使用

第二步:Frida Hook 验证

// 验证找到的解密函数
var mod = Process.findModuleByName("libnative.so");
var candidates = [0x2A10, 0x2B40, 0x2C80];

candidates.forEach(function (offset) {
    var addr = mod.base.add(offset);
    Interceptor.attach(addr, {
        onLeave: function (retval) {
            try {
                var str = retval.readCString();
                if (str && str.length > 0 && str.length < 256) {
                    var hex = "";
                    for (var i = 0; i < Math.min(str.length, 16); i++) {
                        hex += ("0" + str.charCodeAt(i).toString(16))
                            .slice(-2) + " ";
                    }
                    console.log("[0x" + offset.toString(16) +
                        "] len=" + str.length +
                        " hex=" + hex.trim() +
                        " str=\"" + str.substring(0, 50) + "\"");
                }
            } catch (e) {}
        }
    });
});

第三步:批量收集并分类

触发 APP 的各个功能模块(登录、支付、数据同步等),收集所有解密后的字符串,按照 URL、密钥、路径等分类存储,为后续的协议分析和算法还原提供线索。

总结

OLLVM 字符串加密通过在编译时加密、运行时解密的方式保护 SO 文件中的敏感字符串。使用 Frida 动态 Hook 解密函数或 libc 字符串操作函数,可以在运行时批量恢复所有明文字符串。这种方法不依赖静态分析对混淆的去除,是应对字符串加密最直接有效的手段。结合自动化脚本和关键词过滤,可以快速提取出 APP 中的 API 地址、加密密钥等关键信息。